/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package nsip.components;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import java.util.logging.Level;
import java.util.logging.Logger;

import nsip.NsipAgent;
import nsip.components.ds.InventoryDataBase;
import nsip.model.ProductInfo;
import nsip.model.RFQSet;
import nsip.model.Item;
import nsip.model.ItemStatus;
import nsip.scheduling.AbstractSchedule;
import nsip.scheduling.ScheduleCrossover;
import nsip.model.messages.EndOfDay;
import nsip.scheduling.ScheduleEvaluator;
import nsip.scheduling.ScheduleFactory;
import nsip.transport.NsipTransport;
import org.uncommons.maths.random.MersenneTwisterRNG;
import org.uncommons.maths.random.Probability;
import org.uncommons.watchmaker.framework.EvaluatedCandidate;
import org.uncommons.watchmaker.framework.EvolutionEngine;
import org.uncommons.watchmaker.framework.EvolutionObserver;
import org.uncommons.watchmaker.framework.GenerationalEvolutionEngine;
import org.uncommons.watchmaker.framework.PopulationData;
import org.uncommons.watchmaker.framework.SelectionStrategy;
import org.uncommons.watchmaker.framework.selection.RouletteWheelSelection;
import org.uncommons.watchmaker.framework.termination.ElapsedTime;
import se.sics.tasim.props.DeliverySchedule;
import se.sics.tasim.props.FactoryStatus;
import se.sics.tasim.props.ProductionSchedule;
import se.sics.tasim.props.StartInfo;

/**
 * 
 * @author nico.rehwaldt
 */
public class ProductionAgent extends NsipAgent {
    
    public static final double ACCEPTED_ORDERS_PROBABILTY = 0.85;
    public static final double CUSTOMER_RFQ_PROBABILTY = ACCEPTED_ORDERS_PROBABILTY + 0.10;
    public static final double GAPS_PROBABILITY = 1.0;

    public static final int INITIAL_POPULATION = 100;
    public static final int ELITISM = 10;
    public static final int DURATION_IN_MILLIS = 2300;

    private static final Logger logger = Logger.getLogger("ProductionAgent");

    private String factoryAddress;
    private int factoryCapacity;

    private ProductionSchedule lastProductionSchedule;
    
    private int lastBidDueDate;
    
    public ProductionAgent(InformationSystem is,
                           NsipTransport transport) {

        super(is, transport);

        this.lastProductionSchedule = new ProductionSchedule();
    }

    /**
     * Allows the implementation of an agent to reject rfqs which are
     * totally unacceptable for him and thus to reduce the "problem space".
     *
     * @param rfq
     *
     * @return true if the agent accepts the specified rfq, false otherwise
     */
    public boolean acceptsRFQ(Item rfq) {
        return (rfq.getDueDate() < lastBidDueDate);
    }

    /**
     * Compute acceptable rfq sets from the specified rfqs
     * 
     * @param rfqs
     * @return
     */
    public List<RFQSet> computeRFQSets(List<Item> rfqs) {
        ScheduleFactory initialPopulation = new ScheduleFactory(
                is().getCustomerDB().getOrders(ItemStatus.OPEN), rfqs, 1.0, is().getCurrentDate());
        ScheduleCrossover crossoverOperator = new ScheduleCrossover(Probability.ONE, rfqs);
        ScheduleEvaluator evaluator = new ScheduleEvaluator();
        SelectionStrategy<Object> selectionStragety = new RouletteWheelSelection();
        Random rng = new MersenneTwisterRNG();

        EvolutionEngine<AbstractSchedule> evolutionEngine = new GenerationalEvolutionEngine<AbstractSchedule>(
                initialPopulation, crossoverOperator, evaluator, selectionStragety, rng);
        
        MyEvolutionObserver observer = new MyEvolutionObserver();
        evolutionEngine.addEvolutionObserver(observer);
        
        List<EvaluatedCandidate<AbstractSchedule>> schedules =
            evolutionEngine.evolvePopulation(INITIAL_POPULATION, ELITISM, new ElapsedTime(DURATION_IN_MILLIS));

        Collections.sort(schedules, new Comparator<Comparable>() {
            public int compare(Comparable o1, Comparable o2) {
                return o1.compareTo(o2) * -1;
            }
        });

        ArrayList<RFQSet> rfqSets = new ArrayList();

        schedules.get(0).getFitness();
        double maxFitness = -1;
        int count = 0;

        for (EvaluatedCandidate<AbstractSchedule> schedule: schedules) {
            if (maxFitness < schedule.getFitness()) {
                maxFitness = schedule.getFitness();
            }

            logger.fine("[Schedule Fitness] " + schedule.getFitness() + " for " + schedule.getCandidate());
            
            // TODO: Check if no order was skipped
            RFQSet set = new RFQSet(schedule.getCandidate().getPlacedRfqs());
            
            set.addRating(schedule.getFitness() / maxFitness);
            rfqSets.add(set);

            // Only return the 30 best items
            if (++count == 30) {
                break;
            }
        }

        logger.info("[Schedule Evolution] Performed " + observer.getCount() + " evolutions");
        return rfqSets;
    }
    
    private void doDailyPostProcessing() {
        InventoryDataBase inventory = is().getInventory();
        
        // Update the inventory with the goods produced today
        for (int i = 0; i < lastProductionSchedule.size(); i++) {
            inventory.tomorrow().add(
                lastProductionSchedule.getProductID(i),
                lastProductionSchedule.getQuantity(i)
            );
        }

        // The inventory for next day is calculated with todays deliveries
        // and production and is changed when production and delivery
        // requests are made.
        
        // Generate production and delivery schedules
        int currentDate = is().getCurrentDate();
        int latestDueDate = currentDate - is().getDaysBeforeVoid() + 2;
        int availableFactoryCycles = factoryCapacity;
        
        List<Item> orders = is().getCustomerDB().getOrders(ItemStatus.OPEN, ItemStatus.PRODUCED);
        
        // TODO: Do the planning here right!
        ProductionSchedule productionSchedule = new ProductionSchedule();
        DeliverySchedule deliverySchedule = new DeliverySchedule();

        StringBuilder statusLog = new StringBuilder();
        statusLog.append("[Statuslog] ");

        Iterator<Item> iterator = orders.iterator();
        while (iterator.hasNext()) {
            Item order = iterator.next();
            
            int productID = order.getProductID();
            int dueDate = order.getDueDate();
            int quantity = order.getQuantity();
            int inventoryQuantity = inventory.tomorrow().get(productID);
            
            if (currentDate >= (dueDate - 1) &&
                dueDate >= latestDueDate &&
                inventory.reserveForNextDay(productID, quantity)) {

                // It was time to deliver this order and it could be
                // delivered (the method above ensures this). The order has
                // automatically been marked as delivered and the products
                // have been removed from the inventory status (to avoid
                // delivering the same products again).

                if (logger.isLoggable(Level.FINE)) {
                    statusLog.append("delivered(" + order.getId() + "); ");
                }
                
                deliverySchedule.addDelivery(
                    productID, quantity,
                    order.getId(), order.getPeerAddress());
                
                order.setStatus(ItemStatus.PROCESSED);
                iterator.remove();
            } else if (dueDate <= latestDueDate) {
                if (logger.isLoggable(Level.FINE)) {
                    statusLog.append(
                            "canceled(" + order.getId() + ", " +
                            "dueDate: " + order.getDueDate() + ", " +
                            "date: " + currentDate + "); ");
                }

                // It is too late to produce and deliver this order
                is().cancelCustomerOrder(order);
                iterator.remove();
            } else if (inventory.reserveForNextDay(productID, quantity)) {
                if (logger.isLoggable(Level.FINE)) {
                    statusLog.append(
                        "reserved(" + order.getId() +
                        ", due_in: " + (order.getDueDate() - currentDate) + "); ");
                }
                
                // There is enough products in the inventory to fulfill this
                // order and nothing more should be produced for it. However
                // to avoid reusing these products for another order they
                // must be reserved.
                order.setStatus(ItemStatus.PRODUCED);
            } else {
                ProductInfo info = is().getCatalog().getInfo(productID);
                Integer[] componentIDs = info.getComponentIDs();
                
                int quantityToProduce = quantity - inventoryQuantity;
                
                if ((availableFactoryCycles >=
                    info.getRequiredAssemblyCycles() * quantityToProduce) &&
                    inventory.tomorrow().containsComponents(componentIDs, quantityToProduce)) {

                    if (logger.isLoggable(Level.FINE)) {
                        statusLog.append("producting(" + order.getId() + "); ");
                    }
                    // Remove components from tomorrows inventory
                    for (int partID: componentIDs) {
                        inventory.tomorrow().remove(partID, quantityToProduce);
                    }

                    // Track
                    availableFactoryCycles -=
                        (quantityToProduce * info.getRequiredAssemblyCycles());
                    
                    // Add production of good to schedule
                    productionSchedule.addProduction(productID, quantityToProduce);
                    
                    // The method above will ensure that the needed components
                    // was available and that the factory had enough free
                    // capacity. It also removed the needed components from the
                    // inventory status.

                    // Any existing products have been allocated to this order
                    // and must be reserved to avoid using them in another
                    // production or delivery.
                    inventory.reserveForNextDay(productID, inventoryQuantity);
                } else {
                    // Otherwise the production could not be done (lack of
                    // free factory cycles or not enough components in
                    // inventory) and nothing can be done for this order at
                    // this time.
                }
            }
        }

        if (logger.isLoggable(Level.FINE)) {
            logger.fine(statusLog.toString());

            logger.fine(" ------------------------- ");
            logger.fine("[Available quantity left] " + availableFactoryCycles);
            logger.fine("[Production Schedule] " + productionSchedule);
            logger.fine("[Delivery Schedule] " + deliverySchedule);
            logger.fine("[Planned inventory tomorrow] " + inventory.tomorrow());
            logger.fine(" ------------------------- ");
        }
        
        transport().sendMessage(factoryAddress, productionSchedule);
        transport().sendMessage(factoryAddress, deliverySchedule);

        // Set old production schedule to current schedule as next
        // invocation will be next day
        lastProductionSchedule = productionSchedule;
    }

    @Override
    public void receive(Object message) {

        if (message instanceof EndOfDay) {
            doDailyPostProcessing();
        } else

        if (message instanceof FactoryStatus) {
            // Handle factory updates
        } else
        
        if (message instanceof StartInfo) {
            StartInfo gameInfo = (StartInfo) message;

            lastBidDueDate = gameInfo.getNumberOfDays() - 1;
            // Start information for manufacturers
            factoryAddress = gameInfo.getAttribute("factory.address");
            factoryCapacity = gameInfo.getAttributeAsInt("factory.capacity", 2000);
        }
    }

    @Override
    public Collection<Class> getMessageInterests() {        
        return Arrays.asList(new Class[] {
            StartInfo.class,
            FactoryStatus.class,
            EndOfDay.class
        });
    }

    private static class MyEvolutionObserver implements EvolutionObserver {
        private int count = 0;
        
        public void populationUpdate(PopulationData pd) {
            count++;
        }

        public int getCount() {
            return count;
        }
    }

}
